/*
Copyright 2011 Google Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
*/

goog.provide('testing.FpsTestCase');

// Only required by all_tests.html:
goog.require('goog.userAgent.product');
goog.require('goog.testing.MultiTestRunner');

// Required by everything else:
goog.require('goog.testing.jsunit');
goog.require('goog.asserts');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.asserts');
goog.require('testing.FrameRate');
goog.require('testing.KeyNav');

/**
 * A class that enhances {goog.testing.AsyncTestCase} in order to drive tests
 * by frame rate. Rather than calling this constructor, new instances should be
 * created using the {createAndInstall} method.
 *
 * @param {string=} opt_name A descriptive name for the test case.
 * @extends {goog.testing.AsyncTestCase}
 * @constructor
 */
testing.FpsTestCase = function(opt_name) {
  goog.testing.AsyncTestCase.call(this, opt_name);

  /**
   * Display the entire frame sample set that was created while waiting for
   * stable (during setup) for each test method.
   * @type {boolean}
   */
  this.showLastSamples = false;

  /**
   * Display the entire frame sample set that was created while running each
   * test method.
   * @type {boolean}
   */
  this.showSamples = false;

  /**
   * Amount of time in milliseconds for each async delay to wait before
   * triggering a timeout failure.
   * @type {number}
   */
  this.stepTimeout = 30000;

  /**
   * The FrameRate instance that will be used by tests.
   * @type {testing.FrameRate}
   */
  this.frameRate = null;

  /**
   * The HTML element that will be used to display the framerate during test
   * execution.
   * @type {Element}
   */
  this.fpsDisplay = null;

  /**
   * Frequency in milliseconds to update the frame rate display. Setting this
   * value too high will negatively impact playback performance. Too low, and
   * the display loses meaning. Defaults to 500 (or 2 updates per second).
   * @type {number}
   */
  this.updateDisplayInterval = (1000 / 2);
  testing.KeyNav('a');
};

goog.inherits(testing.FpsTestCase, goog.testing.AsyncTestCase);

/**
 * Handler that will be called on each frame render event after setUp and
 * until tearDown is called.
 *
 * @private
 */
testing.FpsTestCase.prototype.getUpdateDisplayHandler_ = function() {
  var self = this;
  return function() {
    //console.log('looping for display with: ' + self.frameRate.getFps());
    var message = self.frameRate.getFps() + ' ' + self.frameRate.getFpsLabel()
        + " (minFpsThreshold: " + self.frameRate.minFpsThreshold + ")";
    goog.dom.setTextContent(goog.dom.$('fps-display'), message);
  }
};

/**
 * Preferred way of creating an FpsTestCase. Creates one and initializes it
 * with the G_testRunner.
 * @param {string=} opt_name A descriptive name for the test case.
 * @return {testing.FpsTestCase} The created FpsTestCase.
 */
testing.FpsTestCase.createAndInstall = function(opt_name) {
  var fpsTestCase = new testing.FpsTestCase(opt_name);
  goog.testing.TestCase.initializeTestRunner(fpsTestCase);
  return fpsTestCase;
};

/**
 * Perform typical setup operations (those that may impact frame rate), and then
 * set up the frame rate instance and wait for the frame rate to "stabilize"
 * before continuing.
 * @override
 */
testing.FpsTestCase.prototype.doSetUp_ = function() {
  testing.FpsTestCase.superClass_.doSetUp_.call(this);

  this.setUpFps();
};


/**
 * Calls the tearDown function, catching any errors, and then moves on to
 * the next step in the testing cycle.
 * @private
 */
testing.FpsTestCase.prototype.doAsyncErrorTearDown_ = function() {
  this.tearDownFps();
  testing.FpsTestCase.superClass_.doAsyncErrorTearDown_.call(this);
};

/**
 * @override
 */
testing.FpsTestCase.prototype.doTearDown_ = function() {
  this.tearDownFps();
  testing.FpsTestCase.superClass_.doTearDown_.call(this);
};

/**
 * Run for {time} milliseconds, and then ensure the minimum frame rate was not
 * breached. Continue running tests after the provided time.
 * @param {number} time in milliseconds to wait.
 * @param {string} message to display if there is a failure.
 */
testing.FpsTestCase.prototype.continueTestingAfter = function(time,
    message) {
  this.waitForAsync(message);
  var self = this;
  setTimeout(function() {
    self.assertFpsNotBreached();
    self.continueTesting();
  }, time);
};

/**
 * Ensure the minimum frame rate was not breached at any point since the test
 * began running. This does not include frames that were collected during setup
 * while we were waiting for 'stable'.
 */
testing.FpsTestCase.prototype.assertFpsNotBreached = function() {
  if (this.frameRate.isAnyFrameBelowThreshold()) {
    fail(this.getFailureMessage());
  }
};

/**
 * Instantiate and return the frame rate display object.
 * @return {Element} that should be added to the DOM.
 */
testing.FpsTestCase.prototype.createFpsDisplay_ = function() {
  var fpsDisplayStyles = [
    "position:absolute;",
    "top:50px;",
    "right:50px;",
    "width:300;",
    "text-align:center;",
    "font-size:1.3em;",
    "color:#fff;",
    "background-color:#000;",
    "border:3px solid #999;",
    "z-index:999;"
  ];
  return goog.dom.createDom('div', {id: 'fps-display',
      style:fpsDisplayStyles.join('')});
};

/**
 * Prepare to run a frame rate test method. This method should be called
 * after all other setup has been completed, and it should pause test
 * execution until the frame rate has become reasonably stable.
 */
testing.FpsTestCase.prototype.setUpFps = function() {
  // Ensure the frameRate instance hasn't been left lingering:
  if (this.frameRate != null) {
    throw "testing.setUpFps expected a null FrameRate instance, did " +
        "you call testing.tearDownFps from your tearDown method?"
  }

  // Create a frame rate view
  this.fpsDisplay = this.createFpsDisplay_();
  // Add the frame rate view to the DOM
  goog.dom.appendChild(document.body, this.fpsDisplay);

  // Create a new FrameRate instance
  this.frameRate = new testing.FrameRate();
  this.frameRate.begin();

  this.updateDisplayIntervalId = setInterval(this.getUpdateDisplayHandler_(),
      this.updateDisplayInterval);

  var self = this;
  var stableHandler = function() {
    self.frameRate.removeEventListener('stable', stableHandler);
    self.continueTesting();
    self.frameRate.reset();
  }

  // Wait for fps to stabilize:
  this.waitForAsync('Waiting for frame rate to stabilize');
  this.frameRate.addEventListener('stable', stableHandler);
  this.frameRate.reset();
};

/**
 * This should be called before any expensive teardown code is executed, and it
 * should capture state from the {testing.FrameRate} entity.
 */
testing.FpsTestCase.prototype.tearDownFps = function() {
  // store these values *before* doing work that might impact them:
  this.avgFrameRate = this.frameRate.getAverageFrameRate();
  this.fpsLabel = this.frameRate.getFpsLabel();
  this.minFrameRate = this.frameRate.min;
  this.maxFrameRate = this.frameRate.max;
  this.historyLabel = this.getHistoryLabel();

  // clean up listeners and fps display and others:
  clearInterval(this.updateDisplayIntervalId);

  goog.dom.setTextContent(goog.dom.$('fps-display'), '');
  this.frameRate.stop();
  goog.dom.removeNode(this.fpsDisplay);
  this.frameRate = null;
};

/**
 * Handles a test that passed.
 * @param {goog.testing.TestCase.Test} test The test that passed.
 * @protected
 */
testing.FpsTestCase.prototype.doSuccess = function(test) {
  this.result_.successCount++;
  var message = test.name + ' : ' + this.getFpsWithLabel();
  this.saveMessage(message);
  this.log(message);
};

/**
 * Will return a failure message that is more helpful for frame rate tests.
 * This message can optionally include the history of frame rate samples that
 * were collected during setup and during the test method if {showLastSamples}
 * and/or {showSamples} are true.
 */
testing.FpsTestCase.prototype.getFailureMessage = function() {
  return 'Minimum FPS threshold breached with: ' + this.frameRate.min + ' '
      + this.frameRate.getFpsLabel() + ' (minFpsThreshold: '
      + this.frameRate.minFpsThreshold + ') ' + this.getHistoryLabel();
};

/**
 * Returns the frame rate with the 'fps' string appended to it.
 * @return {string}
 */
testing.FpsTestCase.prototype.getFpsWithLabel = function() {
  var label = this.fpsLabel;
  return 'PASSED with ' + this.avgFrameRate + ' ' + label + ' (' +
      this.minFrameRate + '/' + this.maxFrameRate + ' ' + label + ' min/max) ' +
      this.historyLabel;
};

/**
 * Figure out if this test case should display setup frame samples, or the
 * test method frame samples based on the {showLastSamples} and {showSamples}
 * attributes, and return an appropriately formatted label based on those
 * settings.
 * @return {string}
 */
testing.FpsTestCase.prototype.getHistoryLabel = function() {
  var parts = [];
  if (this.showLastSamples) {
    var lastSamples = this.frameRate.lastFrameSamples;
    parts.push('(last samples length: ' + lastSamples.length + ' details: ' +
        lastSamples.join(', ') + ')');
  }
  if (this.showSamples) {
    var samples = this.frameRate.frameRateSamples;
    parts.push('(hist length: ' + samples.length + ' details: ' +
        samples.join(', ') + ')');
  }
  return parts.join(' ');
};
